package asteroids.model;

import be.kuleuven.cs.som.annotate.*;
import asteroids.ModelException;
import asteroids.Util;
import asteroids.util.Vector;

/**
 * A class representing all objects that appear in a gameworld in the Asteroids
 * game. All SpaceObjects are of circular shape.
 * All aspects related to the position and the radius are worked out
 * defensively.
 * All aspects related to the velocity are worked out totally.
 * 
 * @invar Each SpaceObject is located on a valid position
 *        | isValidPositionX(getPositionX()) && isValidPostionY(getPositionY())
 * @invar Each SpaceObject has a radius larger than the minimum value
 *        | isValidRadius(getRadius())
 * @invar Each SpaceObject has a velocity that does not exceed lightspeed
 *        | isValidVelocity(getVelocity())
 * @invar Each SpaceObject has a mass that is larger than 0
 *        | isValidMass(getMass())
 */

public abstract class SpaceObject {

	/**
	 * Constructor for an object of the class SpaceObject.
	 * 
	 * @param radius
	 *            The radius of the SpaceObject.
	 * @param mass
	 *            The mass of the SpaceObject
	 * @param velocity
	 *            A vector representing the velocity of the SpaceObject
	 * @param position
	 *            A vector representing the position of the SpaceObject
	 * @param world
	 *            The world in which this SpaceObject is located
	 * 
	 * @throws ModelException
	 *             A ModelException is thrown when the radius, the world or the
	 *             position is invalid
	 *             | if( !isValidRadius(radius) || !isValidPosition(position)
	 *             || ! isValidWorld(world) ) throw new ModelException
	 * 
	 * @effect The radius of this SpaceObject will be set to a given radius if
	 *         it's valid. Else an exception is thrown.
	 *         | if(!isValidRadius() ) throw new ModelException
	 *         | else this.radius = radius
	 * 
	 * @effect When the given mass is valid, the mass of this SpaceObject will
	 *         be set to the given mass.
	 *         Else, sets the mass of this SpaceObject to the minimum mass.
	 *         | if(!isValidMass(mass))
	 *         | this.mass = this.setMass(this.getMinimumMass())
	 *         | else this.setMass(mass)
	 * 
	 * @effect If the velocity given is valid, the velocity of this SpaceObject
	 *         is set to the given velocity.
	 *         Else, it will be set to (0,0)
	 *         | if(!isValidVelocity(velocity))
	 *         | this.velocity = new Vector()
	 *         | else this.velocity = velocity
	 * 
	 * @effect When the given world is not valid, a ModelException is thrown
	 *         Else, sets the world of this SpaceObject to the given world
	 *         | if(!isValidWorld(world))
	 *         | throw new ModelException()
	 *         | else this.setWorld(world)
	 * @effect When the given position is invalid, a modelexception is thrown
	 *         Else, sets the position of this world to the given world.
	 *         | if(!isValidPosition(position))
	 *         | throw new ModelException()
	 *         | else this.position = position
	 */
	public SpaceObject(double radius, double mass, Vector velocity, Vector position, World world) throws ModelException {

		if (!isValidRadius(radius)) {
			throw new ModelException("Invalid Radius");
		}
		else {
			this.radius = radius;
		}

		if (!isValidMass(mass)) {
			this.setMass(this.getMinimumMass());
		}
		else {
			this.setMass(mass);
		}

		if (!isValidVelocity(velocity)) {
			this.velocity = new Vector();
		}
		else {
			this.setVelocityX(velocity.getX());
			this.setVelocityY(velocity.getY());
		}

		if (!isValidWorld(world)) {
			throw new ModelException("Invalid World");
		}
		else {
			setWorld(world);
		}

		if (!isValidPositionX(position.getX(), this.getRadius(), this.getWorld()) || !isValidPositionY(position.getY(), this.getRadius(), this.getWorld())) {
			throw new ModelException("Invalid Position");
		}
		else this.position = position;
	}

	/**
	 * Constructor for sub-classes of SpaceObject, where the mass is calculated
	 * based on the radius. The mass is here set to zero. Constructors from
	 * subclasses will set the mass.
	 * 
	 * @param radius
	 *            The radius of the SpaceObject.
	 * @param velocity
	 *            A vector representing the velocity of the SpaceObject
	 * @param position
	 *            A vector representing the position of the SpaceObject
	 * @param world
	 *            The world in which this SpaceObject is located
	 * 
	 * @throws ModelException
	 *             A ModelException is thrown when the radius, the world or the
	 *             position is invalid
	 *             | if( !isValidRadius(radius) ||!isValidPosition(position) ||
	 *             ! isValidWorld(world) ) throw new ModelException
	 * 
	 * @effect Calls the extended constructor, with given variables and the mass
	 *         set to 0.
	 *         | this(radius, 0, velocity, position, world);
	 */

	public SpaceObject(double radius, Vector velocity, Vector position, World world) throws ModelException {
		this(radius, 0, velocity, position, world);
	}

	/**
	 * Returns the radius of this SpaceObject.
	 * 
	 * @return The radius of this SpaceObject
	 *         | return this.radius
	 */
	@Basic
	@Immutable
	public double getRadius() {
		return this.radius;
	}

	/**
	 * Checks wether the given radius is a valid radius for the SpaceObject.
	 * Because the minimum radius for each type of SpaceObject can differ, this
	 * method is to be overwritten in subclasses
	 * 
	 * @param radius
	 *            The radius to be checked
	 */
	public abstract boolean isValidRadius(double radius);

	/**
	 * A final double storing the radius of this SpaceObject.
	 */
	private final double radius;

	/**
	 * Return the mass of this SpaceObject
	 * 
	 * @return the mass | return this.mass
	 */
	@Basic
	@Immutable
	public double getMass() {
		return this.mass;
	}

	/**
	 * @param mass
	 *            the mass to set
	 * @post The mass of this ship is equal to the given mass | (new
	 *       this).getMass() == mass
	 */
	@Raw
	public void setMass(double mass) {
		this.mass = mass;
	}

	/**
	 * Override in subclasses
	 * 
	 * @param mass
	 *            The mass to be checked wether it is valid
	 * @return True if the mass is valid (dependant on subclasses)
	 */
	public boolean isValidMass(double mass) {
		return mass > 0;
	}

	/**
	 * Returns the minimum mass of a SpaceObject. Dependant on subclasses.
	 * 
	 * @return The minimum mass of a SpaceObject | return this.minimum_mass
	 */
	public double getMinimumMass() {
		return MINIMUM_MASS;
	}

	/**
	 * Variables storing the current mass and minimum mass of a SpaceObject.
	 */
	private static double MINIMUM_MASS = 0;
	private double mass;

	/**
	 * Returns the x component of the velocity of this SpaceObject
	 * 
	 * @return The x component of the velocity of this SpaceObject | return
	 *         this.velocity.getX()
	 */
	@Basic
	@Immutable
	public double getVelocityX() {
		return this.velocity.getX();
	}

	/**
	 * Sets the x component of the velocity of this ship to a given value
	 * 
	 * @param velX
	 *            The velocity in the x direction to set
	 * @post The x component of the velocity is equal to the given value | (new
	 *       this).getVelocityX == velX
	 */
	@Raw
	public void setVelocityX(double velX) {
		Vector newVel = new Vector(velX, this.getVelocityY());
		if (!this.isValidVelocity(newVel)) {
			this.setTotalVelocity(newVel.getX(), newVel.getY());
		}
		else {
			this.velocity.setX(velX);
		}
	}

	/**
	 * Returns the y component of the velocity of this SpaceObject
	 * 
	 * @return The y component of the velocity of this SpaceObject | return
	 *         this.velocity.getY()
	 */
	@Basic
	@Immutable
	public double getVelocityY() {
		return this.velocity.getY();
	}

	/**
	 * Sets the y component of the velocity of this ship to a given value
	 * 
	 * @param velY
	 *            The velocity in the x direction to set
	 * @post The y component of the velocity is equal to the given value | (new
	 *       this).getVelocityY == velY
	 */
	@Raw
	public void setVelocityY(double velY) {
		Vector newVel = new Vector(this.getVelocityX(), velY);
		if (!this.isValidVelocity(newVel)) {
			this.setTotalVelocity(newVel.getX(), newVel.getY());
		}
		else {
			this.velocity.setY(velY);
		}
	}

	/**
	 * Checks wether a given velocity is valid, i.e. smaller than lightspeed and
	 * it is a valid Vector
	 * 
	 * @param velocity
	 *            The velocity to be checked
	 * @return False is no velocity is entered | if(velocity == null) return
	 *         false
	 * @return True if and only if the norm of the velocity is smaller than
	 *         lightspeed and is a number | return (velocity.getNorm() <=
	 *         LIGHTSPEED) && (velocity.getNorm() != Double.NaN)
	 */
	public boolean isValidVelocity(Vector velocity) {
		if (velocity == null) return false;
		return (velocity.getNorm() <= LIGHTSPEED) && (velocity.getNorm() != Double.NaN);
	}

	/**
	 * Returns the total velocity of this SpaceObject
	 * 
	 * @return The norm of the velocity vector | return this.velocity.getNorm()
	 */
	@Basic
	@Immutable
	public double getTotalVelocity() {
		return this.velocity.getNorm();
	}

	/**
	 * Sets the total velocity of a SpaceObject to a given x and y value.
	 * When the norm of the vector formed by velX and velY exceeds lightspeed, the Vector is scaled down, so that the
	 * norm equals lighspeed.
	 * 
	 * @param velX
	 *            The x component of the velocity
	 * @param velY
	 *            The y component of the velocity
	 * 
	 * @effect When the norm of the vector formed by (velX, velY) exceeds, the vector is scaled to lightspeed
	 *         Else, the norm of the vector is used
	 *         | if(new Vector(velX, velY).getNorm() > LIGHTSPEED) velocity = lightspeed
	 *         | else velocity = new Vector(velX, velY).getNorm()
	 * @effect After the usable velocity is determined, sets the x and y velocity of this SpaceObject to the components
	 *         of the calculated vector
	 *         | this.setVelocityX(velocity * Math.cos(angle));
	 *         | this.setVelocityY(velocity * Math.sin(angle));
	 */
	private void setTotalVelocity(double velX, double velY) {
		Vector vel = new Vector(velX, velY);
		double angle = vel.getAngle();
		double velocity = vel.getNorm();
		if (velocity > LIGHTSPEED) velocity = LIGHTSPEED;
		this.setVelocityX(velocity * Math.cos(angle));
		this.setVelocityY(velocity * Math.sin(angle));
	}

	/**
	 * A vector representing the velocity of a ship. Initialized to avoid
	 * nullpointers. A double to store the value of the lightspeed
	 */
	private Vector velocity = new Vector();
	private static final double LIGHTSPEED = 300000;

	/**
	 * Returns the x position of this SpaceObject
	 * 
	 * @return The x position of this SpaceObject | return this.position.getX()
	 */
	@Basic
	@Immutable
	public double getPositionX() {
		return this.position.getX();
	}

	/**
	 * Sets the x coordinate of the position to a certain value
	 * 
	 * @param posX
	 *            The x coordinate to be set
	 * @post The x coordinate is equal to the given x value | (new this).getX()
	 *       = posX
	 */
	@Raw
	public void setPositionX(double posX) {
		this.position.setX(posX);
	}

	/**
	 * Checks wether a given x coordinate is valid. A coordinate is valid if the
	 * SpaceObject lies completely within the world. If the SpaceObject is not
	 * located in a world, its position is always valid.
	 * 
	 * @param posX
	 *            The x-coordinate to be checked
	 * @param radius
	 *            The radius of the SpaceObject
	 * @param world
	 *            The world in which the SpaceObject is located
	 * 
	 * @return Returns true if the SpaceObject is not located in a world | if
	 *         (world == null) return true
	 * @return Returns true if the ship lies within the boundaries of the world
	 *         | return (posX > radius) && (posX < world.getWidth() - radius)
	 * 
	 */
	public boolean isValidPositionX(double posX, double radius, World world) {
		if (world == null) return true;
		return (posX > radius) && (posX < (world.getWidth() - radius)) && (posX != Double.NaN);
	}

	/**
	 * Sets the y coordinate of the position to a certain value
	 * 
	 * @param posY
	 *            The y coordinate to be set
	 * @post The y coordinate is equal to the given y value | (new this).getY()
	 *       = posY
	 */
	@Raw
	public void setPositionY(double posY) {
		this.position.setY(posY);
	}

	/**
	 * Returns the y position of this SpaceObject
	 * 
	 * @return The y position of this SpaceObject | return this.position.getY()
	 */
	@Basic
	@Immutable
	public double getPositionY() {
		return this.position.getY();
	}

	/**
	 * Checks wether a given y coordinate is valid. A coordinate is valid if the
	 * SpaceObject lies completely within the world. If the SpaceObject is not
	 * located in a world, its position is always valid.
	 * 
	 * @param posY
	 *            The y-coordinate to be checked
	 * @param radius
	 *            The radius of the SpaceObject
	 * @param world
	 *            The world in which the SpaceObject is located
	 * 
	 * @return Returns true if the SpaceObject is not located in a world | if
	 *         (world == null) return true
	 * @return Returns true if the ship lies within the boundaries of the world
	 *         | return (posY > radius) && (posX < world.getHeight() - radius)
	 * 
	 */
	public boolean isValidPositionY(double posY, double radius, World world) {
		if (position == null) return false;
		if (world == null) return true;
		return (posY > radius) && (posY < (world.getHeight() - radius)) && (posY != Double.NaN);
	}

	/**
	 * Moves a SpaceObject, based on its velocity and a parameter for how long
	 * it has to move.
	 * 
	 * @param dt
	 *            The time for how long this SpaceObject has to move
	 * 
	 * @effect this.setPositionX(this.getPositionX() + this.getVelocityX() * dt)
	 * @effect this.setPositionY(this.getPositionY() + this.getVelocityY() * dt)
	 */
	public void move(double dt) {
		if (dt < 0) dt = 0;
		this.setPositionX(Vector.add(position, Vector.scalarProduct(velocity, dt)).getX());
		this.setPositionY(Vector.add(position, Vector.scalarProduct(velocity, dt)).getY());
	}

	/**
	 * A vector containing the x and y position of this SpaceObject
	 */
	private Vector position = new Vector();

	/**
	 * Returns the world this SpaceShip is in
	 * 
	 * @return The world this SpaceShip is in | return this.world
	 */
	@Basic
	@Immutable
	public World getWorld() {
		return this.world;
	}

	/**
	 * Sets the world to a given world
	 * 
	 * @param world
	 *            The world in which this SpaceShip will be set
	 * @post (new this).getWorld() == world
	 */
	@Raw
	public void setWorld(World world) {
		this.world = world;
	}

	/**
	 * Checks wether a world is valid. For now, there are no restrictions on the
	 * world
	 * 
	 * @param world
	 *            The world to be checked
	 * @return Returns true for now, since there are no restrictions | return
	 *         true
	 */
	public boolean isValidWorld(World world) {
		return true;
	}

	/**
	 * A variable storing the world in which this SpaceObject is located
	 */
	private World world;

	/**
	 * Sets the acceleration in the x direction to a certain value
	 * 
	 * @param x
	 *            The x-acceleration to be set
	 * 
	 * @post The acceleration in the x-direction is equal to the given value |
	 *       (new this).getAccelerationX() == x
	 */
	@Raw
	public void setAccelerationX(double x) {
		acceleration.setX(x);
	}

	/**
	 * Returns the acceleration in the x-direction of this SpaceObject
	 * 
	 * @return The acceleration in the x-direction | return acceleration.getX()
	 */
	@Basic
	@Immutable
	public double getAccelerationX() {
		return acceleration.getX();
	}

	/**
	 * Sets the acceleration in the y direction to a certain value
	 * 
	 * @param y
	 *            The y-acceleration to be set
	 * 
	 * @post The acceleration in the y-direction is equal to the given value |
	 *       (new this).getAccelerationY() == y
	 */
	@Raw
	public void setAccelerationY(double y) {
		acceleration.setY(y);
	}

	/**
	 * Returns the acceleration in the x-direction of this SpaceObject
	 * 
	 * @return The acceleration in the x-direction | return acceleration.getX()
	 */
	@Basic
	@Immutable
	public double getAccelerationY() {
		return acceleration.getY();
	}

	/**
	 * A vector storing the acceleration of a SpaceObject
	 */
	private Vector acceleration = new Vector();

	/**
	 * Returns the position where this SpaceObject will collide with an other
	 * SpaceObject
	 * 
	 * @param other
	 *            The other SpaceObject
	 * 
	 * @return Return null when this SpaceObject equals the other | if(this ==
	 *         other) return null
	 * @return Returns null if the time until a collision equals infinity |
	 *         if(time == double.POSITIVE_INFINITY) return null
	 * @return Returns the position (a size-2-array) where two SpaceObjects
	 *         collide | return SpaceObject.move(time).getPosition()
	 */
	public double[] getCollisionPosition(SpaceObject other) {

		if (this == other) return null;

		double time = getTimeToCollision(other);

		if (time == Double.POSITIVE_INFINITY) return null;

		Vector collisionPositionThis = new Vector(this.getPositionX() + this.getVelocityX() * time, this.getPositionY() + this.getVelocityY() * time);
		Vector collisionPositionOther = new Vector(other.getPositionX() + other.getVelocityX() * time, other.getPositionY() + other.getVelocityY() * time);

		Vector distanceBetween = new Vector(collisionPositionOther.getX() - collisionPositionThis.getX(), collisionPositionOther.getY() - collisionPositionThis.getY());
		double angle = distanceBetween.getAngle();

		double[] collision = new double[2];

		collision[0] = collisionPositionThis.getX() + Math.cos(angle) * this.getRadius();
		collision[1] = collisionPositionThis.getY() + Math.sin(angle) * this.getRadius();

		return collision;
	}

	/**
	 * Return the time it will take for this ship to collide with the other
	 * ship.
	 * 
	 * @param other
	 *            The other ship to collide with.
	 * @return The resulting time is not negative and different from Double.NaN
	 *         | Util.fuzzyLeq(0.0,result) && (! Double.isNaN(result))
	 * @return If the resulting time is finite, the distance between both ships
	 *         would be fuzzy equal to zero if they would both move during the
	 *         resulting time. | if (result < Double.POSITIVE_INFINITY) then |
	 *         Util.fuzzyEquals(this.distanceBetween(other,result),0.0)
	 * @return If the resulting distance is finite, the distance between both
	 *         ships would be fuzzy different from zero if they would move for a
	 *         time shorter than the resulting time. | if (result <
	 *         Double.POSITIVE_INFINITY) then | for each time in 0.0..result: |
	 *         if (time < result) | then !
	 *         Util.fuzzyEquals(this.distanceBetween(other,time),0.0)
	 * @return If the resulting time is infinite, this ship is the same as the
	 *         other ship or the distance between both ships would be different
	 *         from zero for each finite time they would move. | if (result ==
	 *         Double.POSITIVE_INFINITY) then | (this == other) || | (for each
	 *         time in 0.0..Double.POSITIVE_INFINITY: | if (!
	 *         Double.isInfinite(time)) then | (!
	 *         Util.fuzzyEquals(this.distanceBetween(other,time),0.0))
	 * @throws NullPointerException
	 *             The other ship is not effective. | other == null
	 */
	public double getTimeToCollision(SpaceObject other) {

		if (other == null) throw new NullPointerException();

		if (other instanceof Bullet) {
			if (((Bullet) other).getSource() == this) return Double.POSITIVE_INFINITY;
		}
		else if (this instanceof Bullet) {
			if (((Bullet) this).getSource() == other) return Double.POSITIVE_INFINITY;
		}

		double sigma2 = Math.pow(this.getRadius() + other.getRadius(), 2);

		Vector deltaV = new Vector(other.getVelocityX() - this.getVelocityX(), other.getVelocityY() - this.getVelocityY());
		Vector deltaR = new Vector(other.getPositionX() - this.getPositionX(), other.getPositionY() - this.getPositionY());

		double dVdR = Vector.dotProduct(deltaV, deltaR);
		double dVdV = Vector.dotProduct(deltaV, deltaV);
		double dRdR = Vector.dotProduct(deltaR, deltaR);
		double d = dVdR * dVdR - dVdV * (dRdR - sigma2);

		double collisionTime = -(dVdR + Math.sqrt(d)) / (dVdV);

		if (Util.fuzzyLessThanOrEqualTo(0, dVdR)) {
			return Double.POSITIVE_INFINITY;
		}
		else if (Util.fuzzyLessThanOrEqualTo(d, 0)) {
			return Double.POSITIVE_INFINITY;
		}
		else {
			return collisionTime;
		}
	}

	/**
	 * Return the position where a ship will collide with a wall
	 * 
	 * @return If timeToCollision equals infinity, returns null |
	 *         if(timeToCollision == double.POSITIVE_INFINITY) return null
	 * @return The position where a spacecraft will collide with a wall
	 */
	public double[] getBoundaryCollisionPosition() {

		double timeToBoundaryCollision = this.getTimeToBoundaryCollision();
		if (timeToBoundaryCollision == Double.POSITIVE_INFINITY) { return null; }

		Vector boundaryCollisionPosition = new Vector(this.getPositionX() + this.getVelocityX() * timeToBoundaryCollision, this.getPositionY() + this.getVelocityY() * timeToBoundaryCollision);
		double[] collisionPosition = new double[2];

		if (Util.fuzzyEquals(boundaryCollisionPosition.getX(), this.getRadius())) {
			collisionPosition[0] = 0;
			collisionPosition[1] = boundaryCollisionPosition.getY();
		}
		else if (Util.fuzzyEquals(boundaryCollisionPosition.getX(), this.getWorld().getWidth() - this.getRadius())) {
			collisionPosition[0] = this.getWorld().getWidth();
			collisionPosition[1] = boundaryCollisionPosition.getY();
		}
		else if (Util.fuzzyEquals(boundaryCollisionPosition.getY(), this.getRadius())) {
			collisionPosition[0] = boundaryCollisionPosition.getX();
			collisionPosition[1] = 0;
		}
		else if (Util.fuzzyEquals(boundaryCollisionPosition.getY(), this.getWorld().getHeight() - this.getRadius())) {
			collisionPosition[0] = boundaryCollisionPosition.getX();
			collisionPosition[1] = this.getWorld().getHeight();
		}

		return collisionPosition;
	}

	/**
	 * Return the time for a SpaceObject to collide with a bound of the world.
	 * 
	 * @return Return infinity if it will not collide | if (velocity == (0,0))
	 *         return double.positive_infinity
	 * @return Return the time to a collision with the wall
	 */
	public double getTimeToBoundaryCollision() {

		Vector collisionWallPosition = new Vector();

		if (Util.fuzzyEquals(0, this.getVelocityX()) && Util.fuzzyEquals(0, this.getVelocityY())) { return Double.POSITIVE_INFINITY; }

		if (this.getVelocityX() > 0) {
			/** When a double is divided by zero, result is positive infinity **/
			collisionWallPosition.setX(this.getWorld().getWidth() - this.getRadius());
		}
		else {
			collisionWallPosition.setX(this.getRadius());
		}

		if (this.getVelocityY() > 0) {
			collisionWallPosition.setY(this.getWorld().getHeight() - this.getRadius());
		}
		else {
			collisionWallPosition.setY(this.getRadius());
		}
		double collisionTimeX = (collisionWallPosition.getX() - this.getPositionX()) / this.getVelocityX();
		double collisionTimeY = (collisionWallPosition.getY() - this.getPositionY()) / this.getVelocityY();

		if (Util.fuzzyEquals(this.getVelocityX(), 0)) {
			return collisionTimeY;
		}
		else if (Util.fuzzyEquals(this.getVelocityY(), 0)) {
			return collisionTimeX;
		}
		else {
			return Math.min(collisionTimeX, collisionTimeY);
		}

	}

	/**
	 * Check whether this ship overlaps with the other ship.
	 * 
	 * @param other
	 *            The other ship to compare with.
	 * @return ... | result == (other != null) && (this.distanceBetween(other) <
	 *         0.0)
	 */
	public boolean checkOverlap(SpaceObject other) {
		if (this == other) return false;
		double distanceBetween = Math.sqrt(Math.pow((this.getPositionX() - other.getPositionX()), 2) + Math.pow(this.getPositionY() - other.getPositionY(), 2));
		double radiusSum = this.getRadius() + other.getRadius();
		return !Util.fuzzyLessThanOrEqualTo(radiusSum, distanceBetween);
	}
}
